﻿using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using NUnit.Framework;
using SLE = System.Linq.Enumerable;
using System.Collections.Generic;

namespace NotifiableLinqTests.FrameWork
{
	public abstract class NotificationTestsBase<T> : TestsBase
	{
		#region Setup STA Real Dispatcher

		static readonly object _isInitializedSync = new object();
		static readonly AutoResetEvent _waitHandle = new AutoResetEvent(false);
		static Application _application;
		static Dispatcher _dispatcher;
		static bool _isInitialized;
		static Thread _thread;
		static Window _window;

		static Action Invoke
		{
			set
			{
				if (_dispatcher != null)
				{
					_dispatcher.Invoke(value);
				}
				else
				{
					value();
				}
			}
		}

		static void Initialize()
		{
			if (!_isInitialized)
			{
				lock (_isInitializedSync)
				{
					if (!_isInitialized)
					{
						_isInitialized = true;
						_thread = new Thread(UiThread) { IsBackground = true, Name = "Test UI Thread", };
						_thread.SetApartmentState(ApartmentState.STA);
						_thread.Start();
						_waitHandle.WaitOne();
					}
				}
			}
		}

		static void UiThread()
		{
			try
			{
				_application = new Application();
				_application.Startup += UiThread_WindowStartup;
				_application.Run();
			}
			catch (ThreadAbortException) {}
			catch (Exception ex)
			{
				Trace.WriteLine(ex.ToString());
				Assert.Fail(ex.ToString());
			}
		}

		static void UiThread_WindowStartup(object sender, StartupEventArgs e)
		{
			_window = new Window { Title = "Testing...", Height = 30 };
			// and now it is safe to release main thread, because every variable are created
			_dispatcher = _window.Dispatcher;
			_waitHandle.Set();
			_window.Show();
		}

		[SetUp]
		public void Setup()
		{
			// Initialize();
		}

		#endregion



		protected static readonly Random _rnd = new Random();
		protected Source<T> _source;
		protected IListenerStragegy _listener;

		public void ExecuteTestCore()
		{
			// если нужна опциональная возможность работы с WPF контролами для проверки их реакции на нотифицируемые коллекции,
			// то без инвока прямо внутри тестового метода не обойтись - ведь этот метод вызывается тестовым движком из
			// своего потока...
			Invoke = delegate
			{
				SetupEnvironment();

				var sw = Stopwatch.StartNew();
				_source.LetsDance(() => _listener.PerformAssert());
				Trace.WriteLine("DanceElapsed: " + sw.Elapsed);
			};
		}

		public void SetupEnvironment() {
			// наполняем исходную нотифицируемую коллекцию
			// она не обязательно должна быть нотифицируемой, нужно еще тестит с обычными массивами
			// объекты тоже обязаны быть нотифицируемые, он могут и это надо слушать
			// в основном тестится нотифицируемая коллекция нотифицируемых объектов в качестве Source
			_source = new Source<T>();
			foreach (var item in SourceInitialize())
			{
				_source.Add(item);
			}
			_source.NotificationTestsBase = this;

			var testQuery = QueryActual(_source);
			if (testQuery is IEnumerable && !(testQuery is string))
			{
				// collection
				_listener = new CollectionListenerStragegy();
			}
			else
			{
				// value
				_listener = new ObjectListenerStragegy();
			}

			_listener.InitializeListener(QuerySut(_source));
			_listener.Expector = () => QueryActual(_source);
		}



		/// <summary>
		/// Наполнить исходную коллекцию
		/// </summary>
		public abstract IEnumerable<T> SourceInitialize();
		
		/// <summary>
		/// Создать новый элемент для коллекции
		/// </summary>
		public abstract T CreateNewItem();
		
		/// <summary>
		/// Получить System Under Test - нотифицируемый результат запроса
		/// Делается один раз за сеанс, потом всё ассертится по результатам нотификации
		/// </summary>
		public abstract object QuerySut(IEnumerable<T> source);
		
		/// <summary>
		/// Получить эталонный результат запроса
		/// Делается каждый раз после изменения сорса, чтобы сравнить с тем, что получилось
		/// в результате нотификаций SUT, либо просто с изначальным SUT даже без нотификации
		/// </summary>
		/// <param name="source"></param>
		/// <returns></returns>
		public abstract object QueryActual(IEnumerable<T> source);
		
		/// <summary>
		/// Изменить свойства тестового объекта (он отнотифицирует, если нужно, результат запроса должен обновиться)
		/// </summary>
		/// <param name="item"></param>
		public abstract void ModifyItem(T item);

		public abstract void Clear();
	}
}